﻿package mlc.avm2.visualizing;
import flash.display.DisplayObject;
import flash.display.Shape;
import flash.display.Sprite;
import flash.display.Stage;
import flash.events.Event;
import flash.Lib;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.text.TextFormat;
import flash.ui.Keyboard;
import haxe.Timer;
import hsl.haxe.Bond;
import hsl.haxe.data.keyboard.KeyCombination;
import hsl.haxe.direct.DirectSignaler;
import hsl.haxe.Signal;
import hsl.haxe.Signaler;
import mlc.avm2.visualizing.writetarget.WriteTarget;
import mlc.haxe.input.ConsoleInputSource;
using hsl.avm2.plugins.KeyboardShortcuts;

/**
 * The view used by the AVM2 console visualizer.
 */
class AVM2ConsoleVisualizerView extends Sprite, implements ConsoleInputSource, implements WriteTarget {
	/**
	 * The alpha of the black background of the view.
	 */
	private static inline var BACKGROUND_ALPHA:Float = .8;
	/**
	 * The height of the input field at the bottom of the view.
	 */
	private static inline var INPUT_FIELD_HEIGHT:Float = 17;
	/**
	 * The string that is used when the view wants to start writing on a new line.
	 */
	private static inline var NEWLINE:String = "\n";
	/**
	 * The number of recent commands that are stored.
	 */
	private static inline var NUMBER_OF_RECENT_COMMANDS:Int = 32;
	/**
	 * The black background of the view.
	 */
	private var background:Shape;
	/**
	 * The bond between the key pressed down signaler of the stage, and the handle key pressed down method. This bond is halted
	 * and resumed when the view is closed and opened, respectively.
	 */
	private var handleKeyPressedDownBond:Bond;
	/**
	 * A signaler that dispatches a signal when the view has been initialized. If the view is initialized, the enforce highest
	 * index method, the resize method and the write method may be called.
	 */
	public var initializedSignaler(default, null):Signaler<Void>;
	public var inputReceivedSignaler(default, null):Signaler<String>;
	/**
	 * The input field at the bottom of the view.
	 */
	private var inputField:TextField;
	/**
	 * The output field at the bottom of the view.
	 */
	private var outputField:TextField;
	/**
	 * The index of the recent command that has been retrieved last. Can range from minus one to the length of the recent
	 * commands property minus one.
	 */
	private var recentCommandIndex:Int;
	/**
	 * The recent commands the user has inputted.
	 */
	private var recentCommands:Array<String>;
	/**
	 * The repositioner which repositiones this view after it resizes.
	 */
	private var repositioner:AVM2ConsoleVisualizerViewRepositioner;
	/**
	 * Creates a new AVM2 console visualizer view.
	 */
	public function new(repositioner:AVM2ConsoleVisualizerViewRepositioner, stage:Stage):Void {
		super();
		// Create the signaler that will be used to let the outside world know that this view has been initialized.
		initializedSignaler = new DirectSignaler<Void>(this);
		// Create the signaler that will be used to let the outside world know that the user entered something in the box.
		inputReceivedSignaler = new DirectSignaler<String>(this);
		this.repositioner = repositioner;
		// Create a list that will store the most recent commands the user inputted.
		recentCommands = new Array<String>();
		// Set visible to false, so the next time the toggle opened method is called this view will become visible.
		visible = false;
	}
	public inline function clearOutput():Void {
		outputField.text = "";
	}
	public function enforceHighestIndex(signal:Signal<Void>):Void {
		var addedObject:DisplayObject = cast(signal.origin, DisplayObject);
		// If the object that was added is a direct child of the stage, and its index is higher than the index of this view, swap
		// the indexes of those two.
		if (addedObject.parent == stage && stage.getChildIndex(addedObject) > stage.getChildIndex(this)) {
			stage.swapChildren(addedObject, this);
		}
	}
	private function handleKeyPressedDown(keyCombination:KeyCombination):Void {
		switch (keyCombination.keyCode) {
			// If the user presses the enter key down, this might count as input.
			case Keyboard.ENTER:
			// If, after trimming, the text in the input field is longer than 0, it is in fact input.
			if (0 != StringTools.ltrim(inputField.text).length) {
				var input:String = trimLinebreak(inputField.text);
				inputReceivedSignaler.dispatch(input);
				if (NUMBER_OF_RECENT_COMMANDS == recentCommands.length) {
					recentCommands.pop();
				}
				// Add the input to the list of recent commands.
				recentCommands.unshift(input);
				recentCommandIndex = -1;
			}
			// If the shift key has not been pressed down, clear the text in the input field.
			if (false == keyCombination.modifierKeysState.shiftKeyDown) {
				inputField.text = "";
			}
			// If the user presses the up button down, put a recent command in the input field.
			case Keyboard.UP:
			if (0 != recentCommands.length) {
				recentCommandIndex++;
				// The recent command index should not get higher than the number of recent commands minus one.
				if (recentCommands.length == recentCommandIndex) {
					recentCommandIndex = recentCommands.length - 1;
				}
				inputField.text = recentCommands[recentCommandIndex];
			}
			// If the user presses the down button down, put a recent command in the input field.
			case Keyboard.DOWN:
			if (0 != recentCommands.length) {
				recentCommandIndex--;
				// The recent command index should not get lower than minus one.
				if (-2 == recentCommandIndex) {
					recentCommandIndex = -1;
				}
				if (-1 != recentCommandIndex) {
					inputField.text = recentCommands[recentCommandIndex];
				}
			}
		}
	}
	private inline function initialize(stage:Stage):Void {
		stage.addChild(this);
		// Create the background.
		background = new Shape();
		background.graphics.beginFill(0, BACKGROUND_ALPHA);
		background.graphics.drawRect(0, 0, 1, 1);
		background.graphics.endFill();
		addChild(background);
		// Create the big output field.
		outputField = new TextField();
		outputField.defaultTextFormat = new TextFormat("_typewriter", 11, 0xFFFFFF);
		outputField.multiline = true;
		outputField.wordWrap = true;
		addChild(outputField);
		// Create the input field at the bottom.
		inputField = new TextField();
		inputField.defaultTextFormat = new TextFormat("_typewriter", 11, 0xFFFFFF);
		inputField.type = TextFieldType.INPUT;
		inputField.background = true;
		inputField.backgroundColor = 0x898989;
		addChild(inputField);
		// Resize everything.
		resize(stage.stageWidth, stage.stageHeight);
		handleKeyPressedDownBond = stage.getKeyPressedDownSignaler().bind(handleKeyPressedDown);
		// Dispatch an initialized signal.
		initializedSignaler.dispatch();
	}
	public function resize(width:Float, height:Float):Void {
		// Resize the output field.
		outputField.width = width;
		outputField.height = height - INPUT_FIELD_HEIGHT;
		outputField.scrollV = outputField.maxScrollV;
		// Resize the input field.
		inputField.width = width;
		inputField.y = height - INPUT_FIELD_HEIGHT;
		// Resize the background.
		background.width = width;
		background.height = height;
		// Call the repositioner, as in some cases the view has to be repositioned.
		repositioner.reposition(this);
	}
	/**
	 * Toggles whether the view is opened (visible) or not.
	 */
	public inline function toggleOpened(stage:Stage):Void {
		// Switch the visibility.
		visible = !visible;
		// If the view is now visible, but hasn't been added to the stage yet, initialize it.
		if (visible && false == stage.contains(this)) {
			initialize(stage);
		}
		// If the view is now visible, make sure the key pressed down signals get handled, and that the focus is on the input
		// field.
		if (visible) {
			handleKeyPressedDownBond.resume();
			stage.focus = inputField;
		// If the view is no longer visible, stop handling the key pressed down signals.
		} else {
			handleKeyPressedDownBond.halt();
		}
	}
	private inline function trimLinebreak(value:String):String {
		return
			if (13 == value.charCodeAt(0)) {
				value.substr(1);
			} else {
				value;
			}
	}
	public function write(value:String, color:UInt, breakline:Bool):Void {
		var start:UInt = outputField.length;
		outputField.appendText(value);
		if (breakline) {
			outputField.appendText(NEWLINE);
		}
		outputField.setTextFormat(new TextFormat(null, null, color), start, outputField.length);
		outputField.scrollV = outputField.maxScrollV;
	}
}